<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
    />
    <link rel="stylesheet" href="./assets/global.css" />
    <style>
      #info {
        position: absolute;
        top: 0;
      }
    </style>
  </head>

  <body>
    <div id="info">
      threejs中的第一人称示例改造 <br />
      通过鼠标移动视角<br />
      WASD移动角色 空格跳起
    </div>
    <div id="container"></div>

    <script type="importmap">
      {
        "imports": {
          "three": "https://gcore.jsdelivr.net/npm/three/build/three.module.js",
          "three/addons/": "https://gcore.jsdelivr.net/npm/three/examples/jsm/"
        }
      }
    </script>

    <script type="module">
      import * as THREE from "three";
      import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
      import { FBXLoader } from "three/addons/loaders/FBXLoader.js";

      import { Octree } from "three/addons/math/Octree.js";
      import { OctreeHelper } from "three/addons/helpers/OctreeHelper.js";
      import { Capsule } from "three/addons/math/Capsule.js";
      import { GUI } from "three/addons/libs/lil-gui.module.min.js";
      const clock = new THREE.Clock();
      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0x88ccee);
      scene.fog = new THREE.Fog(0x88ccee, 0, 50);
      const camera = new THREE.PerspectiveCamera(
        70,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      camera.rotation.order = "YXZ";

      const fillLight1 = new THREE.HemisphereLight(0x8dc1de, 0x00668d, 1.5);
      fillLight1.position.set(2, 1, 1);
      scene.add(fillLight1);

      const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5);
      directionalLight.position.set(-5, 25, -1);
      directionalLight.castShadow = true;
      directionalLight.shadow.camera.near = 0.01;
      directionalLight.shadow.camera.far = 500;
      directionalLight.shadow.camera.right = 30;
      directionalLight.shadow.camera.left = -30;
      directionalLight.shadow.camera.top = 30;
      directionalLight.shadow.camera.bottom = -30;
      directionalLight.shadow.mapSize.width = 1024;
      directionalLight.shadow.mapSize.height = 1024;
      directionalLight.shadow.radius = 4;
      directionalLight.shadow.bias = -0.00006;
      scene.add(directionalLight);

      const container = document.getElementById("container");

      const renderer = new THREE.WebGLRenderer({ antialias: true });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.shadowMap.enabled = true;
      renderer.shadowMap.type = THREE.VSMShadowMap;
      renderer.toneMapping = THREE.ACESFilmicToneMapping;
      container.appendChild(renderer.domElement);

      const GRAVITY = 30;
      const SPHERE_RADIUS = 0.2;
      const STEPS_PER_FRAME = 5;
      const worldOctree = new Octree();
      const playerBornPoint = { x: -2, z: -2 };
      const playerCollider = new Capsule(
        new THREE.Vector3(playerBornPoint.x, 0.35, playerBornPoint.z),
        new THREE.Vector3(playerBornPoint.x, 1.8, playerBornPoint.z),
        0.35
      );
      const playerVelocity = new THREE.Vector3();
      const playerDirection = new THREE.Vector3();

      let playerOnFloor = false;

      const keyStates = {};

      document.addEventListener("keydown", (event) => {
        keyStates[event.code] = true;
      });

      document.addEventListener("keyup", (event) => {
        keyStates[event.code] = false;
      });

      container.addEventListener("mousedown", () => {
        document.body.requestPointerLock();
      });

      document.body.addEventListener("mousemove", (event) => {
        if (document.pointerLockElement === document.body) {
          camera.rotation.y -= event.movementX / 500;
          camera.rotation.x -= event.movementY / 500;
        }
      });

      window.addEventListener("resize", onWindowResize);

      function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize(window.innerWidth, window.innerHeight);
      }

      function playerCollisions() {
        const result = worldOctree.capsuleIntersect(playerCollider);

        playerOnFloor = false;

        if (result) {
          playerOnFloor = result.normal.y > 0;

          if (!playerOnFloor) {
            playerVelocity.addScaledVector(
              result.normal,
              -result.normal.dot(playerVelocity)
            );
          }

          playerCollider.translate(result.normal.multiplyScalar(result.depth));
        }
      }

      function updatePlayer(deltaTime) {
        let damping = Math.exp(-4 * deltaTime) - 1;

        if (!playerOnFloor) {
          playerVelocity.y -= GRAVITY * deltaTime;

          // small air resistance
          damping *= 0.1;
        }

        playerVelocity.addScaledVector(playerVelocity, damping);

        const deltaPosition = playerVelocity.clone().multiplyScalar(deltaTime);
        playerCollider.translate(deltaPosition);

        playerCollisions();

        camera.position.copy(playerCollider.end);
      }

      function getForwardVector() {
        camera.getWorldDirection(playerDirection);
        playerDirection.y = 0;
        playerDirection.normalize();

        return playerDirection;
      }

      function getSideVector() {
        camera.getWorldDirection(playerDirection);
        playerDirection.y = 0;
        playerDirection.normalize();
        playerDirection.cross(camera.up);

        return playerDirection;
      }

      function controls(deltaTime) {
        // gives a bit of air control
        const speedDelta = deltaTime * (playerOnFloor ? 25 : 8);

        if (keyStates["KeyW"]) {
          playerVelocity.add(getForwardVector().multiplyScalar(speedDelta));
        }

        if (keyStates["KeyS"]) {
          playerVelocity.add(getForwardVector().multiplyScalar(-speedDelta));
        }

        if (keyStates["KeyA"]) {
          playerVelocity.add(getSideVector().multiplyScalar(-speedDelta));
        }

        if (keyStates["KeyD"]) {
          playerVelocity.add(getSideVector().multiplyScalar(speedDelta));
        }

        if (playerOnFloor) {
          if (keyStates["Space"]) {
            playerVelocity.y = 10;
          }
        }
      }

      const loader = new GLTFLoader().setPath("./assets/exhibition-hall/");
      loader.load("平面.glb", (gltf) => {
        scene.add(gltf.scene);

        worldOctree.fromGraphNode(gltf.scene);
        // gltf.scene.traverse(child => {
        // 	if (child.isMesh) {
        // 		child.castShadow = true;
        // 		child.receiveShadow = true;
        // 		if (child.material.map) {
        // 			child.material.map.anisotropy = 4;
        // 		}
        // 	}
        // });
        const helper = new OctreeHelper(worldOctree);
        helper.visible = false;
        scene.add(helper);
        animate();
      });
      loader.load(
        "小老鼠.glb",
        (gltf) => {
          scene.add(gltf.scene);
          worldOctree.fromGraphNode(gltf.scene);
        },
        function (xhr) {
          console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
        },
        // called when loading has errors
        function (error) {
          console.log("An error happened", error);
        }
      );

      function teleportPlayerIfOob() {
        if (camera.position.y <= -25) {
          playerCollider.start.set(0, 0.35, 0);
          playerCollider.end.set(0, 1, 0);
          playerCollider.radius = 0.35;
          camera.position.copy(playerCollider.end);
          camera.rotation.set(0, 0, 0);
        }
      }

      function animate() {
        const deltaTime = Math.min(0.05, clock.getDelta()) / STEPS_PER_FRAME;

        // we look for collisions in substeps to mitigate the risk of
        // an object traversing another too quickly for detection.

        for (let i = 0; i < STEPS_PER_FRAME; i++) {
          controls(deltaTime);

          updatePlayer(deltaTime);

          teleportPlayerIfOob();

          checkBeforeObjects();
        }

        renderer.render(scene, camera);

        requestAnimationFrame(animate);
      }

      // 检测面前物体
      function checkBeforeObjects() {
        // 检查屏幕中间对应的物体
        const beforeRaycaster = new THREE.Raycaster();
        beforeRaycaster.setFromCamera({ x: 0, y: 0 }, camera);
        const intersects = beforeRaycaster.intersectObjects(scene.children);
        if (intersects.length > 0) {
          console.log(intersects[0].object.name);
          // 激活F键 唤起详情弹框
        }
      }
    </script>
  </body>
</html>
